#!/usr/bin/env python3
# This alternative Reader.py script was meant to cover not only USB readers but more.
# It can be used to replace Reader.py if you have readers such as
# MFRC522, RDM6300 or PN532.
# Please use the github issue threads to share bugs and improvements
# or create pull requests.

import os.path
import sys

import RPi.GPIO as GPIO
import logging

from evdev import InputDevice, ecodes, list_devices

logger = logging.getLogger(__name__)


def get_devices():
    devices = [InputDevice(fn) for fn in list_devices()]
    devices.append(NonUsbDevice('MFRC522'))
    devices.append(NonUsbDevice('RDM6300'))
    devices.append(NonUsbDevice('PN532'))
    return devices


class NonUsbDevice(object):
    name = None

    def __init__(self, name):
        self.name = name


class UsbReader(object):
    def __init__(self, device):
        self.keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX"
        self.dev = device

    def readCard(self):
        from select import select
        stri = ''
        key = ''
        while key != 'KEY_ENTER':
            select([self.dev], [], [])
            for event in self.dev.read():
                if event.type == 1 and event.value == 1:
                    stri += self.keys[event.code]
                    key = ecodes.KEY[event.code]
        return stri[:-1]


class Mfrc522Reader(object):
    def __init__(self):
        import pirc522
        self.device = pirc522.RFID()
        path = os.path.dirname(os.path.realpath(__file__))
        readmode_uid = False
        if os.path.isfile(path + '/../settings/Rfidreader_Rc522_Readmode_UID'):
            with open(path + '/../settings/Rfidreader_Rc522_Readmode_UID', 'r') as f:
                readmode_uid = f.read().rstrip().split(';', 1)[0] == 'ON'
        self._read_function = self._readCard_normal if readmode_uid else self._readCard_legacy

    def _readCard_legacy(self):
        # Scan for cards
        (error, tag_type) = self.device.request()

        if not error:
            logger.info("Card detected.")
            # Perform anti-collision detection to find card uid
            (error, uid) = self.device.anticoll()
            if not error:
                card_id = ''.join((str(x) for x in uid))
                logger.info(card_id)
                return card_id
        logger.debug("No Device ID found.")
        return None

    def _readCard_normal(self):
        # Scan for cards
        uid = self.device.read_id(as_number=True)
        if not uid:
            logger.debug("No Device ID found.")
            return None
        card_id = str(uid)
        logger.info("Card detected.")
        logger.info(card_id)
        return card_id

    def readCard(self):
        # Scan for cards
        self.device.wait_for_tag()
        return self._read_function()

    @staticmethod
    def cleanup():
        GPIO.cleanup()


class Rdm6300Reader:
    def __init__(self, param=None):
        import serial
        device = '/dev/ttyS0'
        baudrate = 9600
        ser_timeout = 0.1
        self.last_card_id = ''
        try:
            self.rfid_serial = serial.Serial(device, baudrate, timeout=ser_timeout)
            self.serial_SerialException = serial.SerialException
        except serial.SerialException as e:
            logger.error(e)
            exit(1)

        self.number_format = ''
        if param is not None:
            nf = param.get("numberformat")
            if nf is not None:
                self.number_format = nf

    def convert_to_weigand26_when_checksum_ok(self, raw_card_id):
        weigand26 = []
        xor = 0
        for i in range(0, len(raw_card_id) >> 1):
            val = int(raw_card_id[i * 2:i * 2 + 2], 16)
            if (i < 5):
                xor = xor ^ val
                weigand26.append(val)
            else:
                chk = val
        if (chk == val):
            return weigand26
        else:
            return None

    def readCard(self): # noqa C901
        byte_card_id = bytearray()

        try:
            while True:
                try:
                    wait_for_start_byte = True
                    while True:
                        read_byte = self.rfid_serial.read()

                        if (wait_for_start_byte):
                            if read_byte == b'\x02':
                                wait_for_start_byte = False
                        else:
                            if read_byte != b'\x03':        # could get stuck here, check len? check timeout by len == 0??
                                byte_card_id.extend(read_byte)
                            else:
                                break

                    raw_card_id = byte_card_id.decode('ascii')
                    byte_card_id.clear()
                    self.rfid_serial.reset_input_buffer()

                    if len(raw_card_id) == 12:
                        w26 = self.convert_to_weigand26_when_checksum_ok(raw_card_id)
                        if (w26 is not None):
                            # print ("factory code is ignored" ,w26[0])

                            if self.number_format == 'card_id_dec':
                                # this will return a 10 Digit card ID e.g. 0006762840
                                card_id = '{0:010d}'.format((w26[1] << 24) + (w26[2] << 16) + (w26[3] << 8) + w26[4])
                            elif self.number_format == 'card_id_float':
                                # this will return card ID as fraction e.g. 103,12632
                                card_id = '{0:d},{1:05d}'.format(((w26[1] << 8) + w26[2]), ((w26[3] << 8) + w26[4]))
                            else:
                                # this will return the raw (original) card ID e.g. 070067315809
                                card_id = raw_card_id

                            if card_id != self.last_card_id:  # does this still makes sense here?
                                self.last_card_id = card_id  # Means 2nd swipe will not be possible with RDM6300
                                return self.last_card_id     # intentionaly? Good reason for this?

                except ValueError as ve:
                    logger.error(ve)

        except self.serial_SerialException as se:
            logger.error(se)

    def cleanup(self):
        self.rfid_serial.close()


class Pn532Reader:
    def __init__(self):
        from py532lib.i2c import Pn532_i2c
        from py532lib.mifare import Mifare
        from py532lib.mifare import MIFARE_WAIT_FOR_ENTRY
        pn532 = Pn532_i2c() # noqa F841
        self.device = Mifare()
        self.device.SAMconfigure()
        self.device.set_max_retries(MIFARE_WAIT_FOR_ENTRY)

    def readCard(self):
        return str(+int('0x' + self.device.scan_field().hex(), 0))

    def cleanup(self):
        # Not sure if something needs to be done here.
        logger.debug("PN532Reader clean up.")


class Reader(object):
    def __init__(self):
        path = os.path.dirname(os.path.realpath(__file__))
        if not os.path.isfile(path + '/deviceName.txt'):
            sys.exit('Please run RegisterDevice.py first')
        else:
            with open(path + '/deviceName.txt', 'r') as f:
                device_name = f.read().rstrip().split(';', 1)[0]

            if device_name == 'MFRC522':
                self.reader = Mfrc522Reader()
            elif device_name == 'RDM6300':
                # The Rdm6300Reader supports 2 Additional Number Formats which can be choosen
                # by an optional parameter dictionary:
                # {'numberformat':'card_id_float'} or {'numberformat':'card_id_dec'}
                self.reader = Rdm6300Reader()
            elif device_name == 'PN532':
                self.reader = Pn532Reader()
            else:
                try:
                    device = [device for device in get_devices() if device.name == device_name][0]
                    self.reader = UsbReader(device)
                except IndexError:
                    sys.exit('Could not find the device %s.\n Make sure it is connected' % device_name)

    def readCard(self):
        return self.reader.readCard()
